﻿using System;
using System.Reflection;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Threading;

using System.Globalization;
using SBPweb.Modules.Logging;

namespace SBPweb.Modules.Modularity
{
    /// <summary>
    /// Gives capability of loading/unloading of modules
    /// </summary>
	public class ModuleLoader : IPersistable
    { 

		#region Private constants

		private const string CoreModuleGroupName = "Core";
		private const string DefaultModuleGroupName = "Default";

		#endregion

		#region Private members

		private string pApplicationTitle;
		private string pApplicationCompanyName;

		private IModuleHost pHost;
		private ModuleHostType pHostType;

		private ServiceDictionary pServices;
		private ModuleDictionary pModules;
		private SessionDictionary pSession;
		private string pModuleGroupName;

		private volatile ModuleDictionary pAsyncInitModules;
		private volatile ModuleDictionary pSyncInitModules;
		private InitializeModuleHandler pAsyncInitHandler;
		private bool pIsAllModuleLoaded;

		private ModuleLoadingPattern pLoadingPattern;
		private DirectoryInfo pDirectory;
		private ModulesConfiguration pModulesConfig;
        private string pPatternOfModules;
		private ILogManager pLogManager;

        private ApartmentState pThreadApartmentState = ApartmentState.STA;

        #endregion

        #region Constructor logic

        /// <summary>
        /// Initializes a new instance
        /// </summary>
        /// <param name="pathOfModules">path of modules</param>
		/// <param name="patternOfModules">File name pattern of modules</param>
		/// <param name="applicationTitle">application title</param>
        /// <param name="applicationCompanyName">application companyname</param>
		/// <param name="logManager">ILogManager logmanager</param>
        public ModuleLoader(string pathOfModules, string patternOfModules, string applicationTitle, string applicationCompanyName, ILogManager logManager)
        {
            pDirectory = new DirectoryInfo(pathOfModules);
            pPatternOfModules = patternOfModules;
            if (!pDirectory.Exists)
            {
                throw new ArgumentException("Path of modules does not exist!");
            }

			pApplicationCompanyName = applicationCompanyName;
			pApplicationTitle = applicationTitle;

			pServices = new ServiceDictionary();

			pSession = new SessionDictionary();
			pLogManager = logManager;

            LoadModulesConfiguration();
        }

        #endregion

        #region Public properties

        /// <summary>
        /// Dictionary of Modules
        /// </summary>
        public ModuleDictionary Modules
        {
            get
            {
                return pModules;
            }
        }

        /// <summary>
        /// Dictionary of Services
        /// </summary>
        public ServiceDictionary Services
        {
            get
            {
                return pServices;
            }
        }

		/// <summary>
		/// Dictionary of Session
		/// </summary>
		public SessionDictionary Session
		{
			get
			{
				return pSession;
			}
		}

        /// <summary>
        /// Host of application
        /// </summary>
        public IModuleHost Host
        {
            get
            {
                return pHost;
            }
        }

        /// <summary>
        /// Pattern of module files
        /// </summary>
        public string PatternOfModules
        {
            get
            {
                return pPatternOfModules;
            }
        }

        /// <summary>
        /// gets the LoadingPattern
        /// </summary>
        public ModuleLoadingPattern LoadingPattern
        {
            get
            {
                return pLoadingPattern;
            }
            internal set
            {
                pLoadingPattern = value;
            }
        }

        /// <summary>
        /// True if all module loaded
        /// </summary>
        public bool IsAllModuleLoaded
        {
            get
            {
                return pIsAllModuleLoaded;
            }
        }

        /// <summary>
        /// gets the ApplicationTitle
        /// </summary>
		public string ApplicationTitle
		{
			get
			{
				return pApplicationTitle;
			}
		}

        /// <summary>
        /// Gets the ApplicationCompanyName
        /// </summary>
		public string ApplicationCompanyName
		{
			get
			{
				return pApplicationCompanyName;
			}
		}

		/// <summary>
		/// Returns the ModuleInfo list of the currently loaded modulegroup and then core modules
		/// </summary>
		public List<ModuleInfo> CurrentModuleList
		{
			get
			{
				List<ModuleInfo> ret = new List<ModuleInfo>();
				ret.AddRange(pModulesConfig.CoreModules.Items);
				if (pModulesConfig.ModuleGroups.ContainsKey(pModuleGroupName))
				{
					ret.AddRange(pModulesConfig.ModuleGroups[pModuleGroupName].Items);
				}
				return ret;
			}
		}

		/// <summary>
		/// Returns the ModuleInfo dictionary of the currently loaded modulegroup and the core modules
		/// Keys are the module type names
		/// </summary>
		public Dictionary<string, ModuleInfo> CurrentModulesDictionary
		{
			get
			{
				Dictionary<string, ModuleInfo> ret = new Dictionary<string, ModuleInfo>();
				foreach (ModuleInfo mi in pModulesConfig.CoreModules.Items)
				{
					ret.Add(mi.ModuleDescriptor.ModuleTypeName, mi);
				}
				if (pModulesConfig.ModuleGroups.ContainsKey(pModuleGroupName))
				{
					foreach (ModuleInfo mi in pModulesConfig.ModuleGroups[pModuleGroupName].Items)
					{
						ret.Add(mi.ModuleDescriptor.ModuleTypeName, mi);
					}
				}
				return ret;
			}
		}


		/// <summary>
		/// Returns all modulegroups
		/// </summary>
		public List<ModuleGroup> ModuleGroups
		{
			get
			{
				return new List<ModuleGroup>(pModulesConfig.ModuleGroups.Values);
			}
		}

		/// <summary>
		/// Returns the currently selected modulegroup's name
		/// </summary>
		public string CurrentModuleGroupName
		{
			get
			{
				return pModuleGroupName;
			}
		}

		/// <summary>
		/// Returns the list of the supported cultures
		/// </summary>
		public List<CultureInfo> SupportedCultures
		{
			get
			{
				if (pModulesConfig == null)
				{
					return null;
				}
				return pModulesConfig.SupportedCultures;
			}
		}

		#region IPersistable Members

		/// <summary>
		/// Unique key of persistable
		/// </summary>
		public string PersistencyKey
		{
			get 
			{
				return this.GetType().Name;
			}
		}

		/// <summary>
		/// Returns the parent persistable instance
		/// </summary>
		public IPersistable ParentPersistable
		{
			get 
			{
				return null;
			}
		}

		#endregion

        public ApartmentState ThreadApartmentState
        {
            get
            {
                return pThreadApartmentState;
            }
            set
            {
                pThreadApartmentState = value;
            }
        }

        #endregion

        #region Public events

        /// <summary>
        /// This even is fired before every module loading
        /// </summary>
        public event EventHandler<ModuleDescriptorEventArgs> ModuleLoading;
        
        /// <summary>
        /// This even is fired after a module loaded
        /// </summary>
        public event EventHandler<ModuleEventArgs> ModuleLoaded;

		/// <summary>
		/// This event is fired after a module unloaded
		/// </summary>
		public event EventHandler<ModuleEventArgs> ModuleUnloading;

        /// <summary>
        /// This event is fired if any configuration error occured
        /// </summary>
        public event EventHandler<ModuleDescriptorEventArgs> ModuleConfigurationError;

        /// <summary>
        /// This event is fired if any error ocured during loading of a module
        /// </summary>
        public event EventHandler<ModuleEventArgs> ModuleLoadError;

		/// <summary>
		/// This event is fired before the current modulegroup unload
		/// </summary>
		public event EventHandler ModuleGroupUnloading;

		/// <summary>
		/// This event is fired after the current modulegroup unloaded
		/// </summary>
		public event EventHandler ModuleGroupUnloaded;

		/// <summary>
		/// This event is fired before the modulegroup load
		/// </summary>
		public event EventHandler ModuleGroupLoading;

		/// <summary>
		/// This event is fired after the modulegroup loaded
		/// </summary>
		public event EventHandler ModuleGroupLoaded;

        /// <summary>
        /// This event is fired when all async module initialized
        /// </summary>
		public event EventHandler AsyncModulesLoaded;

        /// <summary>
        /// This event is fired when all sync module is initialized
        /// </summary>
		public event EventHandler SyncModulesLoaded;

        #endregion

        #region Public methods

		#region Initialization

        /// <summary>
        /// Initialize Loading of modules
        /// </summary>
        /// <param name="host">host of the application</param>
		/// <param name="moduleGroup">module group to load</param>
        public void Initialize(IModuleHost host, string moduleGroup)
		{
            try
            {
                Thread.CurrentThread.SetApartmentState(pThreadApartmentState);
            }
            catch (Exception ex)
            {
                host.LogManager.WriteInfo(this, String.Format("Could not set thread apartment: {0} - {1}", pThreadApartmentState, ex), "ModuleLoader");
            }

			#region Setting default values

			pIsAllModuleLoaded = false;
			pHost = host;
			pHostType = host.HostType;

			pLoadingPattern = ModuleLoadingPattern.Asynchronous;
			pAsyncInitHandler = new InitializeModuleHandler(InitializeModule);

			pModuleGroupName = moduleGroup;
			if (string.IsNullOrEmpty(pModuleGroupName) || !pModulesConfig.ModuleGroups.ContainsKey(pModuleGroupName))
			{
				if (pModulesConfig.ModuleGroups.ContainsKey(DefaultModuleGroupName))
				{
					pModuleGroupName = DefaultModuleGroupName;
				}
				else
				{
					foreach (string moduleGroupName in pModulesConfig.ModuleGroups.Keys)
					{
						pModuleGroupName = moduleGroupName;
						break;
					}
				}
			}

			#endregion

			#region Checking modules

			List<ModuleInfo> autoLoadModules = new List<ModuleInfo>();
			foreach (ModuleInfo mi in pModulesConfig.CoreModules.Items)
			{
				autoLoadModules.Add(mi);
			}
			if (pModuleGroupName != null && pModulesConfig.ModuleGroups.ContainsKey(pModuleGroupName))
			{
				foreach (ModuleInfo mi in pModulesConfig.ModuleGroups[pModuleGroupName].Items)
				{
					if (mi.AutoLoad)
					{
						autoLoadModules.Add(mi);
					}
				}
			}


			#endregion

			#region Loading modules

			pModules = new ModuleDictionary();

			pAsyncInitModules = new ModuleDictionary();
			pSyncInitModules = new ModuleDictionary();

			if (ModuleGroupLoading != null)
			{
				ModuleGroupLoading(this, new EventArgs());
			}

			foreach (ModuleInfo mi in autoLoadModules)
			{
				IModule module = LoadModule(mi.ModuleDescriptor);
				if (module != null)
				{
					mi.ModuleDescriptor.Loaded = true;
					mi.Module = module;
					if ((pLoadingPattern == ModuleLoadingPattern.Asynchronous) && (module is ModuleBase) && (module as ModuleBase).AsyncLoad)
					{
						pAsyncInitModules.Add(module.TypeName, module);
					}
					else
					{
						pSyncInitModules.Add(module.TypeName, module);
					}
				}
			}

			#endregion

			#region Initialize async modules

			if (pAsyncInitModules.Count > 0)
			{
				lock (pAsyncInitModules)
				{
					foreach (string moduleTypeName in pAsyncInitModules.Keys)
					{
						pAsyncInitHandler.BeginInvoke(pAsyncInitModules[moduleTypeName], new AsyncCallback(InitializeModuleCallback), pAsyncInitModules[moduleTypeName]);
					}
				}
			}

			#endregion

			#region Initialize sync modules

			if (pSyncInitModules.Count > 0)
			{
				foreach (string moduleTypeName in pSyncInitModules.Keys)
				{
					InitializeModule(pSyncInitModules[moduleTypeName]);
					pModules.Add(moduleTypeName, pSyncInitModules[moduleTypeName]);
					lock (pModulesConfig)
					{
						if (pModulesConfig.ModuleDescriptors.ContainsKey(moduleTypeName))
						{
							pModulesConfig.ModuleDescriptors[moduleTypeName].Initialized = true;
						}
					}
				}
				pSyncInitModules.Clear();
			}

			if (SyncModulesLoaded != null)
			{
				SyncModulesLoaded(this, EventArgs.Empty);
			}

			#endregion

			#region Finish loading

			lock (pAsyncInitModules)
			{
				if (pSyncInitModules.Count == 0 && pAsyncInitModules.Count == 0)
				{
					pIsAllModuleLoaded = true;
					if (ModuleGroupLoaded != null)
					{
						ModuleGroupLoaded(this, EventArgs.Empty);
					}
				}
			}

			#endregion

		}

        /// <summary>
        /// Terminate and unload modules
        /// </summary>
        public void Terminate()
        {
            pIsAllModuleLoaded = false;

			#region Unloading modules

			UnloadModuleGroup();

			#endregion

			#region Terminating core modules

			lock (pModulesConfig)
			{
				foreach (ModuleInfo mi in pModulesConfig.CoreModules.Items)
				{
					if (mi.Module != null)
					{
						mi.Module.Terminate();
						mi.ModuleDescriptor.Initialized = false;
					}
				}
			}
			pModules.Clear();

			#endregion

		}

		#endregion

		#region Module Loading/Unloading

		/// <summary>
        /// Load a module
        /// </summary>
		/// <param name="moduleInfo">instance of ModuleInfo</param>
		public void LoadModule(ModuleInfo moduleInfo)
		{
			if (pHost != null && !moduleInfo.ModuleDescriptor.Loaded)
			{
				IModule module = LoadModule(moduleInfo.ModuleDescriptor);
				moduleInfo.Module = module;
				moduleInfo.ModuleDescriptor.Loaded = true;
				InitializeModule(module);
				pModules.Add(moduleInfo.ModuleDescriptor.ModuleTypeName, module);
				moduleInfo.ModuleDescriptor.Initialized = true;
			}
		}

        /// <summary>
        /// Unload a module
        /// </summary>
		/// <param name="moduleInfo">instance of ModuleInfo</param>
		public void UnloadModule(ModuleInfo moduleInfo)
		{
			if (moduleInfo.ModuleDescriptor != null && moduleInfo.ModuleDescriptor.Loaded && moduleInfo.Module != null)
			{
				if (ModuleUnloading != null)
				{
					ModuleUnloading(this, new ModuleEventArgs(moduleInfo.Module));
				}
				pModules[moduleInfo.ModuleDescriptor.ModuleTypeName].Terminate();
				moduleInfo.ModuleDescriptor.Initialized = false;
				pModules[moduleInfo.ModuleDescriptor.ModuleTypeName].Dispose();
				pModules.Remove(moduleInfo.ModuleDescriptor.ModuleTypeName);
				moduleInfo.ModuleDescriptor.Loaded = false;
			}
		}

		/// <summary>
		/// Unloads the current module group
		/// </summary>
		public void UnloadModuleGroup()
		{
			if (!string.IsNullOrEmpty(pModuleGroupName) && pModulesConfig.ModuleGroups.ContainsKey(pModuleGroupName))
			{
				if (ModuleGroupUnloading != null)
				{
					ModuleGroupUnloading(this, new EventArgs());
				}

				foreach (ModuleInfo mi in pModulesConfig.ModuleGroups[pModuleGroupName].Items)
				{
					UnloadModule(mi);
				}
				pModuleGroupName = null;
				pIsAllModuleLoaded = false;

				if (ModuleGroupUnloaded != null)
				{
					ModuleGroupUnloaded(this, new EventArgs());
				}
			}
		}

		/// <summary>
		/// Unloads the current module group and loads the specified module group.
		/// </summary>
		/// <param name="moduleGroupName">name of the module group to load</param>
		public void LoadModuleGroup(string moduleGroupName)
		{
			if (moduleGroupName == null)
			{
				return;
			}
			if (!pModulesConfig.ModuleGroups.ContainsKey(moduleGroupName))
			{
				pLogManager.WriteError(this, string.Format("The moduleGroupName parameter: '{0}' is invalid.", moduleGroupName), "ModuleLoader");
				throw new ArgumentException(string.Format("The moduleGroupName parameter: '{0}' is invalid.", moduleGroupName));
			}
			if (!string.IsNullOrEmpty(pModuleGroupName) && pModulesConfig.ModuleGroups.ContainsKey(pModuleGroupName))
			{

				if (ModuleGroupUnloading != null)
				{
					ModuleGroupUnloading(this, new EventArgs());
				}

				List<string> moduleTypesToLoad = new List<string>();
				foreach (ModuleInfo mi in pModulesConfig.ModuleGroups[moduleGroupName].Items)
				{
					moduleTypesToLoad.Add(mi.ModuleDescriptor.ModuleTypeName);
				}
				foreach (ModuleInfo mi in pModulesConfig.ModuleGroups[pModuleGroupName].Items)
				{
					if (!moduleTypesToLoad.Contains(mi.ModuleDescriptor.ModuleTypeName))
					{
						UnloadModule(mi);
					}
				}
				pIsAllModuleLoaded = false;

				if (ModuleGroupUnloaded != null)
				{
					ModuleGroupUnloaded(this, new EventArgs());
				}
			}
			if (!string.IsNullOrEmpty(moduleGroupName))
			{
				if (pModulesConfig.ModuleGroups.ContainsKey(moduleGroupName))
				{
					Host.LogManager.WriteVerbose(this, string.Format("Loading module group: {0}", moduleGroupName), "ModuleLoader");

					if (ModuleGroupLoading != null)
					{
						ModuleGroupLoading(this, new EventArgs());
					}

					foreach (ModuleInfo mi in pModulesConfig.ModuleGroups[moduleGroupName].Items)
					{
						if (!pModules.ContainsKey(mi.ModuleDescriptor.ModuleTypeName) && mi.AutoLoad)
						{
							try
							{
								IModule module = LoadModule(mi.ModuleDescriptor);
								mi.Module = module;
								mi.ModuleDescriptor.Loaded = true;
							}
                            catch (Exception ex)
							{
								Host.LogManager.WriteCritical(this, string.Format("Cannot load module: {0}, {1}", mi.ModuleDescriptor.ModuleTypeName, ex), "ModuleLoader");
							}
						}
					}
					foreach (ModuleInfo mi in pModulesConfig.ModuleGroups[moduleGroupName].Items)
					{
						if (!pModules.ContainsKey(mi.ModuleDescriptor.ModuleTypeName) && mi.AutoLoad && mi.ModuleDescriptor.Loaded)
						{
							try
							{
								InitializeModule(mi.Module);
								pModules.Add(mi.ModuleDescriptor.ModuleTypeName, mi.Module);
								mi.ModuleDescriptor.Initialized = true;
							}
							catch (Exception ex)
							{
								Host.LogManager.WriteCritical(this, string.Format("Cannot initialize module: {0}, {1}", mi.ModuleDescriptor.ModuleTypeName, ex), "ModuleLoader");
							}
						}
						if (pModules.ContainsKey(mi.ModuleDescriptor.ModuleTypeName) && mi.Module == null)
						{
							mi.Module = pModules[mi.ModuleDescriptor.ModuleTypeName];
						}
					}
					Host.LogManager.WriteVerbose(this, string.Format("Module group {0} loaded", moduleGroupName), "ModuleLoader");
					pModuleGroupName = moduleGroupName;
					pIsAllModuleLoaded = true;

					if (ModuleGroupLoaded != null)
					{
						ModuleGroupLoaded(this, EventArgs.Empty);
					}
				}
			}
		}

        /// <summary>
        /// Initializes pModulesConfig for a specifig folder
        /// </summary>
        /// <returns>The modules configuration instance</returns>
        public ModulesConfiguration LoadModulesConfiguration()
        {
            return pModulesConfig = new ModulesConfiguration(pDirectory.FullName, pPatternOfModules, pLogManager);
        }

		#endregion

		#endregion

		#region Private helper methods

		#region Initialization

		private delegate string InitializeModuleHandler(IModule module);
		private string InitializeModule(IModule module)
		{
			try
			{
				if (module != null)
				{
					module.Initialize();
					if (ModuleLoaded != null)
					{
						ModuleLoaded(this, new ModuleEventArgs(module));
					}
				}
			}
			catch (Exception ex)
			{
                Host.LogManager.WriteCritical(this, String.Format("Could not initialize module: {0} - {1}", module.Name, ex), "ModuleLoader");
                if (ModuleLoadError != null)
                {
                    ModuleLoadError(this, new ModuleEventArgs(module));
                }
                else
                {
                    throw ex;
                }
			}
			return module.TypeName;
		}

		private void InitializeModuleCallback(IAsyncResult result)
		{
			string moduleTypeName = (string)pAsyncInitHandler.EndInvoke(result);
			lock (pAsyncInitModules)
			{
				IModule module = pAsyncInitModules[moduleTypeName];
				pAsyncInitModules.Remove(moduleTypeName);
				pModules.Add(moduleTypeName, module);
				if (pAsyncInitModules.Count == 0)
				{
					if (AsyncModulesLoaded != null)
					{
						AsyncModulesLoaded(this, EventArgs.Empty);
					}
				}
				lock (pSyncInitModules)
				{
					if (pAsyncInitModules.Count == 0 && pSyncInitModules.Count == 0)
					{
						pIsAllModuleLoaded = true;
						if (ModuleGroupLoaded != null)
						{
							ModuleGroupLoaded(this, EventArgs.Empty);
						}
					}
				}
			}
			lock (pModulesConfig)
			{
				if (pModulesConfig.ModuleGroups.ContainsKey(pModuleGroupName))
				{
					ModuleInfo mi = pModulesConfig.ModuleGroups[pModuleGroupName].GetItemByName(moduleTypeName);
					if (mi != null)
					{
						mi.ModuleDescriptor.Initialized = true;
					}
				}
			}
		}

		#endregion

		#region Module loading
		
		private IModule LoadModule(ModuleDescriptor md)
		{
			IModule module = null;
			try
			{
				if (ModuleLoading != null)
				{
					ModuleLoading(this, new ModuleDescriptorEventArgs(md));
				}
				module = CreateModuleInstance(md, new object[] { Host });
				if (module is ModuleBase)
				{
					(module as ModuleBase).TypeName = md.ModuleTypeName;
				}
			}
			catch (Exception ex)
			{
				if (ModuleConfigurationError != null)
				{
					ModuleConfigurationError(this, new ModuleDescriptorEventArgs(md, ex));
				}
				Host.LogManager.WriteCritical(this, ex, "ModuleLoader");
#if DEBUG
                throw ex;
#endif
			}
			return module;
		}

		private IModule CreateModuleInstance(ModuleDescriptor md, object[] args)
		{
			IModule ret = null;
			try
			{
                if (md.ConstructorInfo == null)
                {
                    throw new System.Reflection.TargetException("No suitable constructor found in " + md.ModuleTypeName);
                }
				ret = (IModule)md.ConstructorInfo.Invoke(args);
			}
			catch (Exception ex)
			{
				Host.LogManager.WriteCritical(md.ModuleTypeName, ex, "ModuleLoader");
                throw ex;
			}
			return ret;
		}

        #endregion

		#endregion

	}
}
